原文連結:Using Encapsulation to Ensure Consistency
在物件導向的程式中,當一個物件代表了現實世界中的物件,像是員工、汽車及飛機等,會有許多描述該物件的屬性,像代表承運公司的carrier_id
以及航班編號flight_number
。
一般而言,這些與現實有固定組合的值不應被隨意修改,必須要有額外方法阻擋及檢查修改是否被允許。
例如下例中,在lcl_connection
這個class裡,我們可以把carrier_id
及connection_id
設成private或是唯讀值,改用呼叫函式set_attributes
來替屬性賦值。
* 取消原始直接對屬性賦值的方式
* connection->carrier_id = 'XY'. "註解掉"
* connection->connection_id = '0001'. "註解掉"
* 改用呼叫函式來替屬性賦值
connection->set_attributes(...).
這個概念稱為資料封裝。重要資訊僅能透過原始物件本身管理,可以確保沒有外部程式能竄改物件或繞過一致性檢查,這也是物件導向的一大優點。推薦在適當處盡可能的用封裝來保護程式碼!
CLASS <class_name> DEFINTIION.
PUBLIC SECTION.
(CLASS-)DATA <public_attribute> TYPE <type>.
"1. 將屬性設成唯讀"
(CLASS-)DATA <read_only_attribute> TYPE <type> READ-ONLY.
...
"2. 將屬性放在PRIVATE區段"
PRIVATE SECTION.
(CLASS-)DATA <private_attribute> TYPE <type>.
...
ENDCLASS.
方式一:
在public變數後加上READ-ONLY
,表示為唯讀參數,僅能供外部讀取而無法編輯。
方式二:
設成private變數,外部無法讀取和編輯。可以按下Ctrl + 1
後選擇屬性來快速設成private變數
當把屬性設成private或唯讀,可以確保只能用指定的方法來修改屬性,如在下面範例中就是set_attributes()
。然而,仍有兩個潛在的狀況將導致與現實不一致的風險:
DATA connection1 TYPE REF TO lcl_connection.
DATA connection2 TYPE TABLE OF REF TO lcl_connection.
" 風險1. 未呼叫set_attributes( )"
connection1 = NEW #( ).
" 風險2. set_attributes( )被多次呼叫及修改"
connection2 = NEW #( ).
connection2->set_attributes(
carrier_id = 'LH.
connection_id = '0400'.
)
connection2->set_attributes(
carrier_id = 'AA.
connection_id = '0017'.
)
風險一. 由於無法強制程式一定要在實例裡呼叫set_attributes( )
,若沒主動設定屬性,這時connection1
的屬性會被填入**該型別(TYPE)**的初始預設值。
風險二. 由於可以在同一個實例中多次呼叫set_attributes( )
,將造成connection2
的屬性賦值後可能被多次修改。
為了避免使用預設值及多次更改等風險,這時可以用建構函數(constructor method):
CLASS <class_name> DEFINTIION.
PUBLIC SECTION.
"建構函式宣告於PUBLIC區段中"
METHODS constructor
IMPORTING <input_1> TYPE <type>
<input_2> TYPE <type> DEFAULT <val>
...
RASING <exception1>
<exception2>
...
ENDCLASS.
當一個類別存在建構函式,將在實體化同時必定自動呼叫該建構函式,並只會呼叫一次。
在語法上,建構函式擁有以下特徵:
constructor
。注意,在ABAP中,同一個類別只能有一個建構函式。
* Definition內定義建構子
METHODS constructor
IMPORTING
carrier_id TYPE /dmo/carrier_id
connection_id TYPE /dmo/connection_id
* Implementation區段內實體化建構子
METHOD constructor.
me->carrier_id = carrier_id.
me->connection_id = connection_id.
ENDMETHOD.
在上述範例中展示了 lcl_connection
這個類別的建構子,由於建構函式中的參數有著與導入參數相同的名稱,在這裡使用ME
對兩個參數做出區別。
下面範例為建構函數的一些額外設定,可以看需求添加:
Definition內定義建構子
METHODS constructor
IMPORTING
carrier_id TYPE /dmo/carrier_id
connection_id TYPE /dmo/connection_id
"例外事件定義"
RASING
cx_abap_invalid_value.
METHOD constructor.
"檢查是否有被設置初始值"
IF carrier_id IS INITIAL OR connection_id IS INITIAL.
RASING EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
me->carrier_id = carrier_id.
me->connection_id = connection_id.
#計算實體數量
conn_counter = conn_counter + 1.
ENDMETHOD.
初始值檢查
為了避免建立具有有初始值的實例,在實例化時加入例外檢查。
用conn_countor計算實體數量
由於建構函數有著"僅在實例化時呼叫唯一的一次"特性,且靜態屬性的值是跟著class而非實體,在這裡可以設一個特殊的靜態屬性conn_countor
來計數,每當該類別產生一個新實體,conn_countor
就會遞增一次,可以用來計算同一類別實體的總數。
* Definition內定義建構子
METHODS constructor
IMPORTING
carrier_id TYPE /dmo/carrier_id
connection_id TYPE /dmo/connection_id
"例外事件定義"
RASING
cx_abap_invalid_value.
...
* 於NEW#()建立實體時
DATA connection TYPE REF TO lcl_connection.
TRY.
"用表達式NEW #()直接導入參數"
connection = NEW #( carrier_id = 'LH'
connection_id = '0400' ).
CATCH cx_abap_invalid_value.
...
ENDTRY.
延續上個class的設定,在正式透過NEW#()
設定實體時,建構函數會被自動呼叫,一旦存在導入參數數(importing parameters),在NEW#()
中就需提供初始值。語法上跟把參數傳遞到普通方法中一樣。
另外,為了避免建構發生例外狀況,可以加入例外檢查導入參數的型別是否與預設值一致。
註:建構子只能傳入導入參數,EXPORTING關鍵字是不被允許的
動態的建構函數只要建立實體時都會被呼叫一次,但有時你只需要對整個class執行一次設定,這時就需要靜態建構子,也可稱為類別建構子。
靜態建構子則只在該class於第一次被尋址到時,於此時呼叫一次。
第一次被尋址的情況可能發生於:
常見案例會是動態地初始化了一個沒有初始值的靜態屬性,因此在實體化前,最好先呼叫靜態建構子來賦值。
CLASS <class_name> DEFINITION.
PUBLIC SECTION.
CLASS-METHODS class_constructor.
ENDCLASS.
在語法上,靜態建構子有以下特色:
class_constructor
明天正式進入資料庫的環節!